use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::os;
use std::path::{Path, PathBuf};

use crate::record;

type Format = Box<dyn Fn(&mut dyn Write, &record::Record) -> crate::Result<()> + Send>;

pub struct FileAppender {
    file_name: Box<Path>,
    last_roll_hour: u32,
    format: Format,
    file: Option<File>,
}

impl FileAppender {
    pub fn new<P: AsRef<Path>>(file_name: P) -> crate::Result<Self> {
        Ok(Self {
            file_name: {
                let path: Box<Path> = Box::from(file_name.as_ref());
                {
                    let log_dir = path.parent().ok_or("parent is None")?;
                    if !log_dir.exists() {
                        fs::create_dir_all(log_dir)?
                    }
                }
                path
            },
            last_roll_hour: 0,
            format: Box::new(|writer, record| {
                writer.write_all(record.content.as_bytes())?;
                Ok(())
            }),
            file: None,
        })
    }

    pub fn with_format<F>(mut self, format: F) -> Self
    where
        F: Fn(&mut dyn Write, &record::Record) -> crate::Result<()> + 'static + Send,
    {
        self.format = Box::new(format);
        self
    }

    pub fn build(self) -> super::Appender {
        super::Appender::FileAppender(self)
    }

    pub fn name(&self) -> &str {
        "file_appender"
    }

    #[cfg(unix)]
    fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> crate::Result<()> {
        os::unix::fs::symlink(src, dst)?;
        Ok(())
    }
    #[cfg(windows)]
    fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> crate::Result<()> {
        os::windows::fs::symlink_file(src, dst)?;
        Ok(())
    }

    #[cfg(unix)]
    fn gen_file_name(&self, time: &chrono::DateTime<chrono::Local>) -> Option<PathBuf> {
        Some(PathBuf::from(format!(
            "{}.{}",
            self.file_name.to_str()?,
            time.format("%Y%m%d-%H")
        )))
    }

    #[cfg(windows)]
    fn gen_file_name(&self, time: &chrono::DateTime<chrono::Local>) -> Option<PathBuf> {
        let parent = self.file_name.parent()?.to_str()?;
        let file_stem = self.file_name.file_stem()?.to_str()?;
        let log_extension = self.file_name.extension()?.to_str()?;
        Some(PathBuf::from(format!(
            "{}/{}.{}.{}",
            parent,
            file_stem,
            time.format("%Y%m%d-%H"),
            log_extension
        )))
    }

    fn create_file(&mut self, time: &chrono::DateTime<chrono::Local>) -> crate::Result<()> {
        let formated_file_name = self.gen_file_name(time).ok_or("gen_file_name failed")?;
        let file = OpenOptions::new()
            .write(true)
            .append(true)
            .create(true)
            .open(&formated_file_name)?;
        drop(fs::remove_file(&self.file_name));
        if !cfg!(windows) {
            //no symlink in windows because need permissions
            Self::symlink_file(
                &formated_file_name
                    .file_name()
                    .ok_or("formated_file_name is None")?,
                &self.file_name,
            )?;
        }
        self.file = Some(file);
        Ok(())
    }

    pub fn process(&mut self, record: &record::Record) -> crate::Result<()> {
        let cur_secs = record.time.timestamp();
        let cur_hours = (cur_secs / 3600) as u32;
        if cur_hours > self.last_roll_hour {
            self.create_file(&record.time)?;
            self.last_roll_hour = cur_hours;
        }
        let file = self.file.as_mut().ok_or("file as_mut is None")?;
        (self.format)(file, record)?;
        Ok(())
    }
}

impl PartialEq for FileAppender {
    fn eq(&self, _other: &Self) -> bool {
        true
    }
}
